﻿using System;
using System.Collections.Generic;

using Obtics.Values;

using Supremacy.Annotations;
using Supremacy.Collections;
using Supremacy.Scripting;

using System.Linq;

using Supremacy.Scripting.Utility;

namespace Supremacy.Effects
{
    [Serializable]
    public sealed class EffectGroup
    {
        private readonly Type _sourceType;
        private readonly IEffectActivation _activation;
        private readonly IEffectScope _scope;
        private readonly IIndexedCollection<Effect> _effects;
        private readonly IEffectParameterBindingCollection _customParameterBindings;
        private readonly Lazy<EffectParameter> _sourceParameter;
        private readonly Lazy<EffectParameter> _targetParameter;
        private readonly Lazy<ScriptParameters> _customScriptParameters;
        private readonly Lazy<RuntimeScriptParameters> _customRuntimeScriptParameters;
        private readonly Lazy<ScriptExpression> _activationScript;
        private readonly Lazy<ScriptExpression> _scopeScript;
        private readonly Lazy<string> _activationDescription;
        private readonly Lazy<string> _scopeDescription;
        private readonly Lazy<ScriptParameter> _sourceScriptParameter;
        private readonly Lazy<ScriptParameter> _targetScriptParameter;
        private readonly Lazy<ScriptParameters> _systemScriptParameters;
        private readonly Lazy<EffectParameterCollection> _systemParameters;

        private EffectGroup()
        {
            _sourceParameter = new Lazy<EffectParameter>(this.CreateSourceParameter);
            _sourceScriptParameter = new Lazy<ScriptParameter>(() => _sourceParameter.Value.ToScriptParameter());
            _targetParameter = new Lazy<EffectParameter>(this.CreateTargetParameter);
            _targetScriptParameter = new Lazy<ScriptParameter>(() => _targetParameter.Value.ToScriptParameter());
            _customScriptParameters = new Lazy<ScriptParameters>(this.CreateCustomScriptParameters);
            _customRuntimeScriptParameters = new Lazy<RuntimeScriptParameters>(this.CreateCustomRuntimeScriptParameters);

            _activationScript = new Lazy<ScriptExpression>(this.EvaluateActivation);
            _scopeScript = new Lazy<ScriptExpression>(this.EvaluateScope);
            _activationDescription = new Lazy<string>(this.EvaluateActivationDescription);
            _scopeDescription = new Lazy<string>(this.EvaluateScopeDescription);

            _systemParameters = new Lazy<EffectParameterCollection>(
                () => new EffectParameterCollection
                      {
                          _sourceParameter.Value,
                          _targetParameter.Value
                      });

            _systemScriptParameters = new Lazy<ScriptParameters>(
                () => new ScriptParameters(_sourceScriptParameter.Value, _targetScriptParameter.Value));
        }

        private EffectGroup(
            [NotNull] Type sourceType,
            [CanBeNull] IEffectActivation activation,
            [NotNull] IEffectScope scope,
            [NotNull] IIndexedCollection<Effect> effects,
            [CanBeNull] IEffectParameterBindingCollection customParameterBindings)
            : this()
        {
            if (sourceType == null)
                throw new ArgumentNullException("sourceType");
            if (scope == null)
                throw new ArgumentNullException("scope");
            if (effects == null)
                throw new ArgumentNullException("effects");

            if (!typeof(IEffectSource).IsAssignableFrom(sourceType))
            {
                throw new ArgumentException(
                    string.Format(
                        "Source type must implement '{0}'.",
                        typeof(IEffectSource).FullName),
                    "sourceType");
            }

            _sourceType = sourceType;
            _activation = activation;
            _scope = scope;
            _effects = effects;
            _customParameterBindings = customParameterBindings ?? new EffectParameterBindingCollection();

            _effects.ForEach(o => o.EffectGroup = this);
        }
        
        internal EffectParameterCollection SystemParameters
        {
            get { return _systemParameters.Value; }
        }

        internal ScriptParameter SourceScriptParameter
        {
            get { return _sourceScriptParameter.Value; }
        }

        internal ScriptParameters SystemScriptParameters
        {
            get { return _systemScriptParameters.Value; }
        }

        public IEffectActivation Activation
        {
            get { return _activation; }
        }

        public IEffectScope Scope
        {
            get { return _scope; }
        }

        public IIndexedCollection<Effect> Effects
        {
            get { return _effects; }
        }

        public IEffectParameterBindingCollection CustomParameterBindings
        {
            get { return _customParameterBindings; }
        }

        internal ScriptParameters CustomScriptParameters
        {
            get { return _customScriptParameters.Value; }
        }

        internal RuntimeScriptParameters CustomRuntimeScriptParameters
        {
            get { return _customRuntimeScriptParameters.Value; }
        }

        internal ScriptExpression ActivationScript
        {
            get { return _activationScript.Value; }
        }

        internal ScriptExpression ScopeScript
        {
            get { return _scopeScript.Value; }
        }
        
        internal string ActivationDescription
        {
            get { return _activationDescription.Value; }
        }

        internal string ScopeDescription
        {
            get { return _scopeDescription.Value; }
        }

        public EffectGroupBinding Bind(IEffectSource source)
        {
            return new EffectGroupBinding(
                source,
                this,
                this.CustomParameterBindings);
        }

        private ScriptExpression EvaluateActivation()
        {
            if (_activation == null)
                return null;

            return new ScriptExpression
                   {
                       ScriptCode = _activation.ValueExpression,
                       Parameters = new ScriptParameters(this.SourceScriptParameter)
                   };
        }

        private ScriptExpression EvaluateScope()
        {
            return new ScriptExpression
                   {
                       ScriptCode = _scope.ValueExpression,
                       Parameters = new ScriptParameters(_sourceScriptParameter.Value)
                   };
        }

        private string EvaluateActivationDescription()
        {
            if (_activation == null || string.IsNullOrWhiteSpace(_activation.DescriptionExpression))
                return null;

            var script = new ScriptExpression
                         {
                             ScriptCode = _activation.DescriptionExpression,
                             Parameters = new ScriptParameters(this.CustomScriptParameters)
                         };

            var result = script.Evaluate(this.CustomRuntimeScriptParameters);
            
            var stringValueProvider = result as IValueProvider<string>;
            if (stringValueProvider != null)
                return stringValueProvider.Value;

            var valueProvider = result as IValueProvider;
            if (valueProvider != null)
                return valueProvider.Value as string;

            return result as string;
        }

        private string EvaluateScopeDescription()
        {
            if (string.IsNullOrWhiteSpace(_scope.DescriptionExpression))
                return null;

            var script = new ScriptExpression
                         {
                             ScriptCode = _scope.DescriptionExpression,
                             Parameters = new ScriptParameters(this.CustomScriptParameters)
                         };

            var result = script.Evaluate(this.CustomRuntimeScriptParameters);

            var stringValueProvider = result as IValueProvider<string>;
            if (stringValueProvider != null)
                return stringValueProvider.Value;

            var valueProvider = result as IValueProvider;
            if (valueProvider != null)
                return valueProvider.Value as string;

            return result as string;
        }

        private EffectParameter CreateTargetParameter()
        {
            var targetType = this.ScopeScript.ReturnType;

            if (targetType.IsGenericType)
            {
                var openType = TypeManager.DropGenericTypeArguments(targetType);
                if (typeof(IValueProvider<>).IsAssignableFrom(openType))
                    targetType = targetType.GetGenericArguments()[0];
            }

            if (targetType.IsGenericType)
            {
                var openType = TypeManager.DropGenericTypeArguments(targetType);
                if (typeof(IEnumerable<>).IsAssignableFrom(openType))
                    targetType = targetType.GetGenericArguments()[0];
            }

            return new EffectParameter(Effect.ParameterNameTarget, targetType);
        }

        private EffectParameter CreateSourceParameter()
        {
            return new EffectParameter(Effect.ParameterNameSource, _sourceType);
        }

        private ScriptParameters CreateCustomScriptParameters()
        {
            return new ScriptParameters(this.CustomParameterBindings.Select(o => o.Parameter.ToScriptParameter()));
        }

        private RuntimeScriptParameters CreateCustomRuntimeScriptParameters()
        {
            return this.CustomParameterBindings.ToRuntimeScriptParameters(this.CustomScriptParameters);
        }

        public static EffectGroup Create(
            Type sourceType,
            IEffectActivation activation,
            IEffectScope scope,
            IIndexedCollection<Effect> effects,
            IEffectParameterBindingCollection customParameters)
        {
            return new EffectGroup(sourceType, activation, scope, effects, customParameters);
        }
    }
}